package client.net;

import java.io.IOException;
import java.util.Hashtable;
import java.util.Vector;

import javax.bluetooth.L2CAPConnection;
import javax.bluetooth.L2CAPConnectionNotifier;
import javax.bluetooth.RemoteDevice;
import javax.bluetooth.UUID;
import javax.microedition.io.Connector;
import javax.swing.event.EventListenerList;

import client.container.BtPortContainer;
import client.container.FileCompletedContainer;
import client.event.NewPositionEvent;
import client.event.NewSenderEvent;
import client.event.PortProviderListener;
import client.event.SessionManagerListener;

import com.intel.bluetooth.BlueCoveImpl;
import com.intel.bluetooth.RemoteDeviceHelper;

/**
 * @author primianoc
 *
 */
public class PortProvider extends Thread 
{
	private UUID L2CAP_SERVER_UUID = new UUID(0x0100);

	private String _urlL2capLocal = null; 

	private BtPortContainer _btPortContainer = BtPortContainer.getInstance();

	private SessionManagerListener _sessionManagerListener = null;

	private boolean _providerActive = true;

	private int _l2capSessionManagerCounter = 0;

	private String _l2capMainPsmString = "4097"; 

	private String _bluecoveDeviceID = null;

	private Vector<SessionManager> _sessionManagerVector = new Vector<SessionManager>();

	private EventListenerList _eventListenerList = new EventListenerList(); 

	private Hashtable<String, Integer> _positionHashTable = new Hashtable<String, Integer>();

	private Hashtable<String, Long> _obsoleteHashTable = new Hashtable<String, Long>();

	private FileCompletedContainer _fileCompletedContainer = FileCompletedContainer.getInstance();

	/**
	 * @param active the _providerActive to set
	 */
	public void setProviderActive(boolean active)
	{
		_providerActive = active;
	}

	public PortProvider(SessionManagerListener sessionManagerListener, 
			String bluecoveDeviceID)
	{	
		_urlL2capLocal =  "btl2cap://" +
		"localhost" +
		":" + 
		L2CAP_SERVER_UUID.toString() +
		";name=L2CAP Psm Provider"+  // Nome del servizio che sto mettendo a disposizione
		";bluecovepsm=" + //Assegno un PSM specifico al server
		_l2capMainPsmString;

		/**
		 *  La main form verra' registrata come ascoltatore degli eventi 
		 *  generati dai session manager.
		 */
		_sessionManagerListener = sessionManagerListener;

		/**
		 *  Dispositivo bluetooth da utilizzare
		 */
		_bluecoveDeviceID = bluecoveDeviceID;
	}

	@Override
	public void run() 
	{
		/**
		 *  Reset stack bluetooth.
		 */

		BlueCoveImpl.shutdown();

		/**
		 * Seleziono il dispositivo bluetooth da utilizzare.
		 */
		BlueCoveImpl.setConfigProperty("bluecove.deviceID", _bluecoveDeviceID);

		L2CAPConnectionNotifier l2capConnectionNotifier = null;

		L2CAPConnection l2capConnection = null;

		while(_providerActive == true)
		{
			/**
			 *  Apro un server L2CAP in attesa di connessioni.
			 * Quando la connessione viene aperta spedisco una stringa 
			 * contenente la porta su cui il session manager riceverà 
			 * lo stream RTP oppure ricevo un valore RSSI.
			 */
			try
			{
				if(l2capConnectionNotifier == null)
				{
					l2capConnectionNotifier = 
						(L2CAPConnectionNotifier)Connector.open(_urlL2capLocal);
				}

				l2capConnection = (L2CAPConnection)l2capConnectionNotifier.acceptAndOpen();

				/**
				 * Dispositivo remoto che ha effettuato la connessione.
				 */
				RemoteDevice remoteDevice = RemoteDeviceHelper.getRemoteDevice(l2capConnection);

				/** 
				 * Leggo una stringa che può assumere due formati:
				 * 1) RSSI,64,file2
				 * 2) file2,137
				 * La prima contiene il valore RSSI 64 rilevato 
				 * da un Gateway in posizione file2, 
				 * la seconda contiene una richiesta di una porta 
				 * per trasmettere lo stream relativo alla posizione 
				 * file2 con durata 137 secondi.
				 */
				byte buffer[] = new byte[80];
				int bytes_read = l2capConnection.receive(buffer);
				String stringaRicevuta = new String(buffer, 0, bytes_read);

				String delimitatore = ",";
				String[] stringaRicevutaArray = stringaRicevuta.split(delimitatore);

				String firstValue = stringaRicevutaArray[0];

				int secondValue = Integer.parseInt(stringaRicevutaArray[1]);

				String response = "NOSERVICE";

				if(firstValue.equalsIgnoreCase("RSSI"))
				{
					/**
					 * Aggiornamento RSSI da parte di un gateway.
					 * Formato: RSSI, valoreNumerico, StreamName
					 */
					String thirdValue = stringaRicevutaArray[2];

					/**
					 * Elimino i valori obsoleti, ricevuti più di
					 * 20 secondi fa.
					 */
					for(String key: _obsoleteHashTable.keySet())
					{
						long timeMillis = _obsoleteHashTable.get(key);

						if( System.currentTimeMillis() > (timeMillis + 20000))
						{
							_positionHashTable.remove(key);
							_obsoleteHashTable.remove(key);
						}
					}

					/**
					 * Se ho già ricevuto un valore RSSI dal Gateway allora
					 * lo devo aggiornare.
					 */
					if(_positionHashTable.containsKey(thirdValue) == true)
					{
						_positionHashTable.remove(thirdValue);
					}
					else
					{
						if(_positionHashTable.size() > 1)
						{
							int tempValue = 0;
							String tempKey = null;

							for(String key: _positionHashTable.keySet())
							{
								int value = _positionHashTable.get(key);

								if( value >= tempValue)
								{
									tempValue = value;
									tempKey = key;
								}
							}

							_positionHashTable.remove(tempKey);
						}
						else
						{
							/**
							 * RSSI relativo ad una nuova posizione.
							 */
							_obsoleteHashTable.put(thirdValue, System.currentTimeMillis());
						}
					}

					_positionHashTable.put(thirdValue, secondValue);


					/*
					 * Calcolo del gateway più vicino.
					 */
					int tempValue = Integer.MAX_VALUE;
					String tempKey = null;

					for(String key: _positionHashTable.keySet())
					{
						int value = _positionHashTable.get(key);

						if( value <= tempValue)
						{
							tempValue = value;
							tempKey = key;
						}
					}

					/*
					 * Scateno l'evento che permette alla GUI di posizionare il segnaposto
					 * dell'utente.
					 */
					fireNewPosition(tempKey);

					response= "ACK";

				}
				else
				{
					/*
					 * Richiesta di una porta da parte di un gateway.
					 */
					String btPort = null;

					if(_fileCompletedContainer.
							isStreamCompleted(firstValue) == false)
					{

						/**
						 * Il metodo getAllBtPort() restituisce le porte disponibili.
						 */

						String[] btPorts= _btPortContainer.getAllBtPort();

						if(btPorts.length > 0)
						{
							int i=0;

							/**
							 *  Se ci sono porte disponibili provo ad inviarne una al 
							 *  gateway. Se la accetta istanzio un SessionManager.
							 */
							while(i < btPorts.length && _providerActive == true && 
									response.equalsIgnoreCase("NOSERVICE") == true)
							{
								buffer = new byte[80];

								btPort = btPorts[i];

								l2capConnection.send(btPort.getBytes());

								/**
								 *  Ricevo la risposta del gateway.
								 */
								l2capConnection.receive(buffer);

								String feedBack = new String(buffer, 0, bytes_read);

								if(feedBack.contains("ACK"))
								{	
									/**
									 * Rimuovo la porta assegnata al gateway dal contenitore.
									 */
									_btPortContainer.extractBtPsm(btPort);

									SessionManager sessionManager = null;

									/**
									 * Se il gateway sta eseguendo un usurpo allora
									 * bisogna aggiungere uno stream al SessionManager
									 * che ha lo stesso streamName.
									 */
									for (SessionManager sessionMngr: _sessionManagerVector)
									{
										if(sessionMngr.getStreamName()
												.equalsIgnoreCase(firstValue))
										{
											sessionManager = sessionMngr;
										}
									}

									if(sessionManager == null)
									{
										/**
										 *  Se non ho trovato un SessionManager che 
										 *  gestisce lo stream allora ne creo uno nuovo.
										 *  I parametri sono la durata e il nome dello 
										 *  stream.
										 */
										sessionManager = 
											new SessionManager(secondValue, firstValue);

										/**
										 * Aggiungo il SessionManager al vettore.
										 */
										_sessionManagerVector.add(sessionManager);

										/**
										 *  Contatore dei thread SessionManager utilizzato
										 *  per assegnare un nome diverso ad ogni thread.
										 */
										_l2capSessionManagerCounter = _l2capSessionManagerCounter + 1;

										sessionManager.setName("SessionManager numero " + 
												_l2capSessionManagerCounter);

										/**
										 *  Aggiungo ai listener del thread l'ascoltatore che notifica 
										 *  alla GUI gli eventi del thread SessionManager.
										 */
										sessionManager.addSessionManagerListener(_sessionManagerListener);

										sessionManager.start();
									}

									/**
									 *  Inserisco il primo stream nella sessione, identificato dalla
									 *  porta appena inviata al gateway.
									 */
									sessionManager.addStream(btPort);

									response = "ACK";

								}// fine if(feedBack.contains("ACK"))

								// Incremento l'indice dell'array delle porte.
								i++;
							}//fine while(i < btPorts.length && _providerActive == true ...)
						}	//fine if(btPorts.length > 0)
					}
					else
					{
						/**
						 * Se il nome dello stream è presente nel contenitore
						 * dei file completati allora non c'è bisogno di creare 
						 * un SessionManager. 
						 */
						response = "COMPLETED";
					}

					if(response.equalsIgnoreCase("NOSERVICE") == true || 
							response.equalsIgnoreCase("COMPLETED"))
					{
						btPort = "NONE";
					}

					/*
					 * Scateno l'evento che aggiorna la GUI.
					 */
					fireNewSender(remoteDevice.getBluetoothAddress(), btPort);
				}

				/**
				 * Invio la risposta al Gateway.
				 */
				l2capConnection.send(response.getBytes());

				/**
				 *  Chiudo la connessione.
				 */
				l2capConnection.close();
				l2capConnection = null;
			}
			catch (Exception exception) 
			{
				/**
				 * Se _providerActive è false allora disattivo tutti i SessionManager.
				 */
				if(_providerActive == false)
				{
					for (SessionManager sessionManager : _sessionManagerVector)
					{
						if(sessionManager != null)
						{
							removeSessionManager(sessionManager);
						}
					}
				}

				/**
				 *  In caso di eccezioni chiudo il notifier e la connection.
				 */
				if(l2capConnectionNotifier!=null)
				{
					try
					{
						l2capConnectionNotifier.close();
					}
					catch ( IOException e )
					{
						e.printStackTrace();
					}

					l2capConnectionNotifier = null;
				}

				if(l2capConnection!=null)
				{
					try 
					{
						l2capConnection.close();
						l2capConnection = null;
					}
					catch (IOException e) 
					{
						e.printStackTrace();
					}
				}
			}
		}

		/**
		 *  Se sono uscito dal ciclo while allora devo terminare 
		 *   tutti gli stack bluetooth aperti.
		 */
		BlueCoveImpl.shutdown();
	}

	private void removeSessionManager(SessionManager sessionManager)
	{
		_sessionManagerVector.remove(sessionManager);

		sessionManager.close();
	}

	public void removeSessionManager(String streamName)
	{
		SessionManager sessionManagerTmp = null;
		for (SessionManager sessionManager: _sessionManagerVector)
		{
			if(sessionManager.getStreamName().equalsIgnoreCase(streamName))
			{
				sessionManagerTmp = sessionManager;

			}
		}

		if(sessionManagerTmp != null)
		{
			removeSessionManager(sessionManagerTmp);
		}
	}

	/**
	 * Metodi che permettono ai listener di registrarsi per intercettare
	 * gli eventi generati dal PortProvider.
	 */
	public void addPortProviderListener(PortProviderListener listener)
	{
		_eventListenerList.add(PortProviderListener.class, listener);
	}

	public void removePortProviderListener(PortProviderListener listener)
	{
		_eventListenerList.remove(PortProviderListener.class, listener);
	}

	/**
	 * Lancia l'evento che indica all'interfaccia grafica 
	 * la posizione assunta dal Client.
	 * @param newPosition
	 */
	private void fireNewPosition(String newPosition)
	{
		NewPositionEvent newPositionEvent  = 
			new NewPositionEvent(this, newPosition);

		/**
		 * Per ogni listener che si e' precedentemente registrato all'evento
		 * invoco il metodo portProviderNewPosition passando come parametro 
		 * l'evento newPositionEvent.
		 */
		Object[] listenersArray = _eventListenerList.getListenerList();

		for(int i = listenersArray.length - 2; i >= 0; i = i - 2)
		{
			if(listenersArray[i] == PortProviderListener.class)
			{
				((PortProviderListener)listenersArray[i+1])
				.portProviderNewPosition(newPositionEvent);
			} 
		}

	}

	/**
	 * Lancia l'evento che indica all'interfaccia grafica 
	 * l'aggiornamento del Gateway Sender.
	 */
	private void fireNewSender(String senderAddress, String dataPort)
	{	
		NewSenderEvent newSenderEvent = 
			new NewSenderEvent(this, senderAddress,  dataPort);

		/**
		 * Per ogni listener che si e' precedentemente registrato all'evento
		 * invoco il metodo portProvideNewSender passando come parametro 
		 * l'evento newSenderEvent.
		 */
		Object[] listenersArray = _eventListenerList.getListenerList();

		for(int i = listenersArray.length - 2; i >= 0; i = i - 2)
		{
			if(listenersArray[i] == PortProviderListener.class)
			{
				((PortProviderListener)listenersArray[i+1])
				.portProvideNewSender(newSenderEvent);
			} 
		}

	}

}
